/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

#pragma once

#include <juce_core/system/juce_TargetPlatform.h>

#if JUCE_PLUGINHOST_ARA && (JUCE_MAC || JUCE_WINDOWS || JUCE_LINUX)

#include <JuceHeader.h>

#include <ARA_API/ARAInterface.h>
#include <ARA_Library/Dispatch/ARAHostDispatch.h>

class FileAudioSource
{
    auto getAudioSourceProperties() const
    {
        auto properties = ARAHostModel::AudioSource::getEmptyProperties();
        properties.name = formatReader->getFile().getFullPathName().toRawUTF8();
        properties.persistentID = formatReader->getFile().getFullPathName().toRawUTF8();
        properties.sampleCount = formatReader->lengthInSamples;
        properties.sampleRate = formatReader->sampleRate;
        properties.channelCount = (int) formatReader->numChannels;
        properties.merits64BitSamples = false;
        return properties;
    }

public:
    FileAudioSource (ARA::Host::DocumentController& dc, const juce::File& file)
        : formatReader ([&file]
          {
              auto result = rawToUniquePtr (WavAudioFormat().createMemoryMappedReader (file));
              result->mapEntireFile();
              return result;
          }()),
          audioSource (Converter::toHostRef (this), dc, getAudioSourceProperties())
    {
        audioSource.enableAudioSourceSamplesAccess (true);
    }

    bool readAudioSamples (float* const* buffers, int64 startSample, int64 numSamples)
    {
        // TODO: the ARA interface defines numSamples as int64. We should do multiple reads if necessary with the reader.
        if (numSamples > std::numeric_limits<int>::max())
            return false;

        return formatReader->read (buffers, (int) formatReader->numChannels, startSample, (int) (numSamples));
    }

    bool readAudioSamples (double* const* buffers, int64 startSample, int64 numSamples)
    {
        ignoreUnused (buffers, startSample, numSamples);
        return false;
    }

    MemoryMappedAudioFormatReader& getFormatReader() const { return *formatReader; }

    auto getPluginRef() const { return audioSource.getPluginRef(); }

    auto& getSource() { return audioSource; }

    using Converter = ARAHostModel::ConversionFunctions<FileAudioSource*, ARA::ARAAudioSourceHostRef>;

private:
    std::unique_ptr<MemoryMappedAudioFormatReader> formatReader;
    ARAHostModel::AudioSource audioSource;
};

//==============================================================================
class MusicalContext
{
    auto getMusicalContextProperties() const
    {
        auto properties = ARAHostModel::MusicalContext::getEmptyProperties();
        properties.name = "MusicalContext";
        properties.orderIndex = 0;
        properties.color = nullptr;
        return properties;
    }

public:
    MusicalContext (ARA::Host::DocumentController& dc)
        : context (Converter::toHostRef (this), dc, getMusicalContextProperties())
    {
    }

    auto getPluginRef() const { return context.getPluginRef(); }

private:
    using Converter = ARAHostModel::ConversionFunctions<MusicalContext*, ARA::ARAMusicalContextHostRef>;

    ARAHostModel::MusicalContext context;
};

//==============================================================================
class RegionSequence
{
    auto getRegionSequenceProperties() const
    {
        auto properties = ARAHostModel::RegionSequence::getEmptyProperties();
        properties.name = name.toRawUTF8();
        properties.orderIndex = 0;
        properties.musicalContextRef = context.getPluginRef();
        properties.color = nullptr;
        return properties;
    }

public:
    RegionSequence (ARA::Host::DocumentController& dc, MusicalContext& contextIn, String nameIn)
        : context (contextIn),
          name (std::move (nameIn)),
          sequence (Converter::toHostRef (this), dc, getRegionSequenceProperties())
    {
    }

    auto& getMusicalContext() const { return context; }
    auto getPluginRef() const { return sequence.getPluginRef(); }

private:
    using Converter = ARAHostModel::ConversionFunctions<RegionSequence*, ARA::ARARegionSequenceHostRef>;

    MusicalContext& context;
    String name;
    ARAHostModel::RegionSequence sequence;
};

class AudioModification
{
    auto getProperties() const
    {
        auto properties = ARAHostModel::AudioModification::getEmptyProperties();
        properties.persistentID = "x";
        return properties;
    }

public:
    AudioModification (ARA::Host::DocumentController& dc, FileAudioSource& source)
        : modification (Converter::toHostRef (this), dc, source.getSource(), getProperties())
    {
    }

    auto& getModification() { return modification; }

private:
    using Converter = ARAHostModel::ConversionFunctions<AudioModification*, ARA::ARAAudioModificationHostRef>;

    ARAHostModel::AudioModification modification;
};

//==============================================================================
class PlaybackRegion
{
    auto getPlaybackRegionProperties() const
    {
        auto properties = ARAHostModel::PlaybackRegion::getEmptyProperties();
        properties.transformationFlags = ARA::kARAPlaybackTransformationNoChanges;
        properties.startInModificationTime = 0.0;
        const auto& formatReader = audioSource.getFormatReader();
        properties.durationInModificationTime = (double) formatReader.lengthInSamples / formatReader.sampleRate;
        properties.startInPlaybackTime = 0.0;
        properties.durationInPlaybackTime = properties.durationInModificationTime;
        properties.musicalContextRef = sequence.getMusicalContext().getPluginRef();
        properties.regionSequenceRef = sequence.getPluginRef();

        properties.name = nullptr;
        properties.color = nullptr;
        return properties;
    }

public:
    PlaybackRegion (ARA::Host::DocumentController& dc,
                    RegionSequence& s,
                    AudioModification& m,
                    FileAudioSource& source)
        : sequence (s),
          audioSource (source),
          region (Converter::toHostRef (this), dc, m.getModification(), getPlaybackRegionProperties())
    {
        jassert (source.getPluginRef() == m.getModification().getAudioSource().getPluginRef());
    }

    auto& getPlaybackRegion() { return region; }

private:
    using Converter = ARAHostModel::ConversionFunctions<PlaybackRegion*, ARA::ARAPlaybackRegionHostRef>;

    RegionSequence& sequence;
    FileAudioSource& audioSource;
    ARAHostModel::PlaybackRegion region;
};

//==============================================================================
class AudioAccessController final : public ARA::Host::AudioAccessControllerInterface
{
public:
    ARA::ARAAudioReaderHostRef createAudioReaderForSource (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                                           bool use64BitSamples) noexcept override
    {
        auto audioReader = std::make_unique<AudioReader> (audioSourceHostRef, use64BitSamples);
        auto audioReaderHostRef = Converter::toHostRef (audioReader.get());
        auto* readerPtr = audioReader.get();
        audioReaders.emplace (readerPtr, std::move (audioReader));
        return audioReaderHostRef;
    }

    bool readAudioSamples (ARA::ARAAudioReaderHostRef readerRef,
                           ARA::ARASamplePosition samplePosition,
                           ARA::ARASampleCount samplesPerChannel,
                           void* const* buffers) noexcept override
    {
        const auto use64BitSamples = Converter::fromHostRef (readerRef)->use64Bit;
        auto* audioSource = FileAudioSource::Converter::fromHostRef (Converter::fromHostRef (readerRef)->sourceHostRef);

        if (use64BitSamples)
            return audioSource->readAudioSamples (
                reinterpret_cast<double* const*> (buffers), samplePosition, samplesPerChannel);

        return audioSource->readAudioSamples (
            reinterpret_cast<float* const*> (buffers), samplePosition, samplesPerChannel);
    }

    void destroyAudioReader (ARA::ARAAudioReaderHostRef readerRef) noexcept override
    {
        audioReaders.erase (Converter::fromHostRef (readerRef));
    }

private:
    struct AudioReader
    {
        AudioReader (ARA::ARAAudioSourceHostRef source, bool use64BitSamples)
            : sourceHostRef (source), use64Bit (use64BitSamples)
        {
        }

        ARA::ARAAudioSourceHostRef sourceHostRef;
        bool use64Bit;
    };

    using Converter = ARAHostModel::ConversionFunctions<AudioReader*, ARA::ARAAudioReaderHostRef>;

    std::map<AudioReader*, std::unique_ptr<AudioReader>> audioReaders;
};

class ArchivingController final : public ARA::Host::ArchivingControllerInterface
{
public:
    using ReaderConverter = ARAHostModel::ConversionFunctions<MemoryBlock*, ARA::ARAArchiveReaderHostRef>;
    using WriterConverter = ARAHostModel::ConversionFunctions<MemoryOutputStream*, ARA::ARAArchiveWriterHostRef>;

    ARA::ARASize getArchiveSize (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override
    {
        return (ARA::ARASize) ReaderConverter::fromHostRef (archiveReaderHostRef)->getSize();
    }

    bool readBytesFromArchive (ARA::ARAArchiveReaderHostRef archiveReaderHostRef,
                               ARA::ARASize position,
                               ARA::ARASize length,
                               ARA::ARAByte* buffer) noexcept override
    {
        auto* archiveReader = ReaderConverter::fromHostRef (archiveReaderHostRef);

        if ((position + length) <= archiveReader->getSize())
        {
            std::memcpy (buffer, addBytesToPointer (archiveReader->getData(), position), length);
            return true;
        }

        return false;
    }

    bool writeBytesToArchive (ARA::ARAArchiveWriterHostRef archiveWriterHostRef,
                              ARA::ARASize position,
                              ARA::ARASize length,
                              const ARA::ARAByte* buffer) noexcept override
    {
        auto* archiveWriter = WriterConverter::fromHostRef (archiveWriterHostRef);

        if (archiveWriter->setPosition ((int64) position) && archiveWriter->write (buffer, length))
            return true;

        return false;
    }

    void notifyDocumentArchivingProgress (float value) noexcept override { ignoreUnused (value); }

    void notifyDocumentUnarchivingProgress (float value) noexcept override { ignoreUnused (value); }

    ARA::ARAPersistentID getDocumentArchiveID (ARA::ARAArchiveReaderHostRef archiveReaderHostRef) noexcept override
    {
        ignoreUnused (archiveReaderHostRef);

        return nullptr;
    }
};

class ContentAccessController final : public ARA::Host::ContentAccessControllerInterface
{
public:
    using Converter = ARAHostModel::ConversionFunctions<ARA::ARAContentType, ARA::ARAContentReaderHostRef>;

    bool isMusicalContextContentAvailable (ARA::ARAMusicalContextHostRef musicalContextHostRef,
                                           ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (musicalContextHostRef);

        return (type == ARA::kARAContentTypeTempoEntries || type == ARA::kARAContentTypeBarSignatures);
    }

    ARA::ARAContentGrade getMusicalContextContentGrade (ARA::ARAMusicalContextHostRef musicalContextHostRef,
                                                        ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (musicalContextHostRef, type);

        return ARA::kARAContentGradeInitial;
    }

    ARA::ARAContentReaderHostRef
        createMusicalContextContentReader (ARA::ARAMusicalContextHostRef musicalContextHostRef,
                                           ARA::ARAContentType type,
                                           const ARA::ARAContentTimeRange* range) noexcept override
    {
        ignoreUnused (musicalContextHostRef, range);

        return Converter::toHostRef (type);
    }

    bool isAudioSourceContentAvailable (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                        ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (audioSourceHostRef, type);

        return false;
    }

    ARA::ARAContentGrade getAudioSourceContentGrade (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                                     ARA::ARAContentType type) noexcept override
    {
        ignoreUnused (audioSourceHostRef, type);

        return 0;
    }

    ARA::ARAContentReaderHostRef
        createAudioSourceContentReader (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                        ARA::ARAContentType type,
                                        const ARA::ARAContentTimeRange* range) noexcept override
    {
        ignoreUnused (audioSourceHostRef, type, range);

        return nullptr;
    }

    ARA::ARAInt32 getContentReaderEventCount (ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept override
    {
        const auto contentType = Converter::fromHostRef (contentReaderHostRef);

        if (contentType == ARA::kARAContentTypeTempoEntries || contentType == ARA::kARAContentTypeBarSignatures)
            return 2;

        return 0;
    }

    const void* getContentReaderDataForEvent (ARA::ARAContentReaderHostRef contentReaderHostRef,
                                              ARA::ARAInt32 eventIndex) noexcept override
    {
        if (Converter::fromHostRef (contentReaderHostRef) == ARA::kARAContentTypeTempoEntries)
        {
            if (eventIndex == 0)
            {
                tempoEntry.timePosition = 0.0;
                tempoEntry.quarterPosition = 0.0;
            }
            else if (eventIndex == 1)
            {
                tempoEntry.timePosition = 2.0;
                tempoEntry.quarterPosition = 4.0;
            }

            return &tempoEntry;
        }
        else if (Converter::fromHostRef (contentReaderHostRef) == ARA::kARAContentTypeBarSignatures)
        {
            if (eventIndex == 0)
            {
                barSignature.position = 0.0;
                barSignature.numerator = 4;
                barSignature.denominator = 4;
            }

            if (eventIndex == 1)
            {
                barSignature.position = 1.0;
                barSignature.numerator = 4;
                barSignature.denominator = 4;
            }

            return &barSignature;
        }

        jassertfalse;
        return nullptr;
    }

    void destroyContentReader (ARA::ARAContentReaderHostRef contentReaderHostRef) noexcept override
    {
        ignoreUnused (contentReaderHostRef);
    }

    ARA::ARAContentTempoEntry tempoEntry;
    ARA::ARAContentBarSignature barSignature;
};

class ModelUpdateController final : public ARA::Host::ModelUpdateControllerInterface
{
public:
    void notifyAudioSourceAnalysisProgress (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                            ARA::ARAAnalysisProgressState state,
                                            float value) noexcept override
    {
        ignoreUnused (audioSourceHostRef, state, value);
    }

    void notifyAudioSourceContentChanged (ARA::ARAAudioSourceHostRef audioSourceHostRef,
                                          const ARA::ARAContentTimeRange* range,
                                          ARA::ContentUpdateScopes scopeFlags) noexcept override
    {
        ignoreUnused (audioSourceHostRef, range, scopeFlags);
    }

    void notifyAudioModificationContentChanged (ARA::ARAAudioModificationHostRef audioModificationHostRef,
                                                const ARA::ARAContentTimeRange* range,
                                                ARA::ContentUpdateScopes scopeFlags) noexcept override
    {
        ignoreUnused (audioModificationHostRef, range, scopeFlags);
    }

    void notifyPlaybackRegionContentChanged (ARA::ARAPlaybackRegionHostRef playbackRegionHostRef,
                                             const ARA::ARAContentTimeRange* range,
                                             ARA::ContentUpdateScopes scopeFlags) noexcept override
    {
        ignoreUnused (playbackRegionHostRef, range, scopeFlags);
    }
};

class PlaybackController final : public ARA::Host::PlaybackControllerInterface
{
public:
    void requestStartPlayback() noexcept override {}
    void requestStopPlayback() noexcept override {}

    void requestSetPlaybackPosition (ARA::ARATimePosition timePosition) noexcept override
    {
        ignoreUnused (timePosition);
    }

    void requestSetCycleRange (ARA::ARATimePosition startTime, ARA::ARATimeDuration duration) noexcept override
    {
        ignoreUnused (startTime, duration);
    }

    void requestEnableCycle (bool enable) noexcept override { ignoreUnused (enable); }
};

struct SimplePlayHead final : public juce::AudioPlayHead
{
    Optional<PositionInfo> getPosition() const override
    {
        PositionInfo result;
        result.setTimeInSamples (timeInSamples.load());
        result.setIsPlaying (isPlaying.load());
        return result;
    }

    std::atomic<int64_t> timeInSamples { 0 };
    std::atomic<bool> isPlaying { false };
};

struct HostPlaybackController
{
    virtual ~HostPlaybackController() = default;

    virtual void setPlaying (bool isPlaying) = 0;
    virtual void goToStart() = 0;
    virtual File getAudioSource() const = 0;
    virtual void setAudioSource (File audioSourceFile) = 0;
    virtual void clearAudioSource() = 0;
};

class AudioSourceComponent final : public Component,
                                   public FileDragAndDropTarget,
                                   public ChangeListener
{
public:
    explicit AudioSourceComponent (HostPlaybackController& controller, juce::ChangeBroadcaster& bc)
        : hostPlaybackController (controller),
          broadcaster (bc),
          waveformComponent (*this)
    {
        audioSourceLabel.setText ("You can drag and drop .wav files here", NotificationType::dontSendNotification);

        addAndMakeVisible (audioSourceLabel);
        addAndMakeVisible (waveformComponent);

        playButton.setButtonText ("Play / Pause");
        playButton.onClick = [this]
        {
            isPlaying = ! isPlaying;
            hostPlaybackController.setPlaying (isPlaying);
        };

        goToStartButton.setButtonText ("Go to start");
        goToStartButton.onClick = [this] { hostPlaybackController.goToStart(); };

        addAndMakeVisible (goToStartButton);
        addAndMakeVisible (playButton);

        broadcaster.addChangeListener (this);

        update();
    }

    ~AudioSourceComponent() override
    {
        broadcaster.removeChangeListener (this);
    }

    void changeListenerCallback (ChangeBroadcaster*) override
    {
        update();
    }

    void resized() override
    {
        auto localBounds = getLocalBounds();
        auto buttonsArea = localBounds.removeFromBottom (40).reduced (5);
        auto waveformArea = localBounds.removeFromBottom (150).reduced (5);

        juce::FlexBox fb;
        fb.justifyContent = juce::FlexBox::JustifyContent::center;
        fb.alignContent = juce::FlexBox::AlignContent::center;

        fb.items = { juce::FlexItem (goToStartButton).withMinWidth (100.0f).withMinHeight ((float) buttonsArea.getHeight()),
                     juce::FlexItem (playButton).withMinWidth (100.0f).withMinHeight ((float) buttonsArea.getHeight()) };

        fb.performLayout (buttonsArea);

        waveformComponent.setBounds (waveformArea);

        audioSourceLabel.setBounds (localBounds);
    }

    bool isInterestedInFileDrag (const StringArray& files) override
    {
        if (files.size() != 1)
            return false;

        if (files.getReference (0).endsWithIgnoreCase (".wav"))
            return true;

        return false;
    }

    void update()
    {
        const auto currentAudioSource = hostPlaybackController.getAudioSource();

        if (currentAudioSource.existsAsFile())
        {
            waveformComponent.setSource (currentAudioSource);
            audioSourceLabel.setText (currentAudioSource.getFullPathName(),
                                      NotificationType::dontSendNotification);
        }
        else
        {
            waveformComponent.clearSource();
            audioSourceLabel.setText ("You can drag and drop .wav files here", NotificationType::dontSendNotification);
        }
    }

    void filesDropped (const StringArray& files, int, int) override
    {
        hostPlaybackController.setAudioSource (files.getReference (0));
        update();
    }

private:
    class WaveformComponent final : public Component,
                                    public ChangeListener
    {
    public:
        WaveformComponent (AudioSourceComponent& p)
            : parent (p),
              thumbCache (7),
              audioThumb (128, formatManager, thumbCache)
        {
            setWantsKeyboardFocus (true);
            formatManager.registerBasicFormats();
            audioThumb.addChangeListener (this);
        }

        ~WaveformComponent() override
        {
            audioThumb.removeChangeListener (this);
        }

        void mouseDown (const MouseEvent&) override
        {
            isSelected = true;
            repaint();
        }

        void changeListenerCallback (ChangeBroadcaster*) override
        {
            repaint();
        }

        void paint (juce::Graphics& g) override
        {
            if (! isEmpty)
            {
                auto rect = getLocalBounds();

                const auto waveformColour = Colours::cadetblue;

                if (rect.getWidth() > 2)
                {
                    g.setColour (isSelected ? juce::Colours::yellow : juce::Colours::black);
                    g.drawRect (rect);
                    rect.reduce (1, 1);
                    g.setColour (waveformColour.darker (1.0f));
                    g.fillRect (rect);
                }

                g.setColour (Colours::cadetblue);
                audioThumb.drawChannels (g, rect, 0.0, audioThumb.getTotalLength(), 1.0f);
            }
        }

        void setSource (const File& source)
        {
            isEmpty = false;
            audioThumb.setSource (new FileInputSource (source));
        }

        void clearSource()
        {
            isEmpty = true;
            isSelected = false;
            audioThumb.clear();
        }

        bool keyPressed (const KeyPress& key) override
        {
            if (isSelected && key == KeyPress::deleteKey)
            {
                parent.hostPlaybackController.clearAudioSource();
                return true;
            }

            return false;
        }

    private:
        AudioSourceComponent& parent;

        bool isEmpty = true;
        bool isSelected = false;
        AudioFormatManager formatManager;
        AudioThumbnailCache thumbCache;
        AudioThumbnail audioThumb;
    };

    HostPlaybackController& hostPlaybackController;
    juce::ChangeBroadcaster& broadcaster;
    Label audioSourceLabel;
    WaveformComponent waveformComponent;
    bool isPlaying { false };
    TextButton playButton, goToStartButton;
};

class ARAPluginInstanceWrapper final : public AudioPluginInstance
{
public:
    class ARATestHost final : public HostPlaybackController,
                              public juce::ChangeBroadcaster
    {
    public:
        class Editor final : public AudioProcessorEditor
        {
        public:
            explicit Editor (ARATestHost& araTestHost)
                : AudioProcessorEditor (araTestHost.getAudioPluginInstance()),
                  audioSourceComponent (araTestHost, araTestHost)
            {
                audioSourceComponent.update();
                addAndMakeVisible (audioSourceComponent);
                setSize (512, 220);
            }

            ~Editor() override { getAudioProcessor()->editorBeingDeleted (this); }

            void resized() override { audioSourceComponent.setBounds (getLocalBounds()); }

        private:
            AudioSourceComponent audioSourceComponent;
        };

        explicit ARATestHost (ARAPluginInstanceWrapper& instanceIn)
            : instance (instanceIn)
        {
            if (instance.inner->getPluginDescription().hasARAExtension)
            {
                instance.inner->setPlayHead (&playHead);

                createARAFactoryAsync (*instance.inner, [this] (ARAFactoryWrapper araFactory)
                                                        {
                                                            init (std::move (araFactory));
                                                        });
            }
        }

        void init (ARAFactoryWrapper araFactory)
        {
            if (araFactory.get() != nullptr)
            {
                documentController = ARAHostDocumentController::create (std::move (araFactory),
                                                                        "AudioPluginHostDocument",
                                                                        std::make_unique<AudioAccessController>(),
                                                                        std::make_unique<ArchivingController>(),
                                                                        std::make_unique<ContentAccessController>(),
                                                                        std::make_unique<ModelUpdateController>(),
                                                                        std::make_unique<PlaybackController>());

                if (documentController != nullptr)
                {
                    const auto allRoles = ARA::kARAPlaybackRendererRole | ARA::kARAEditorRendererRole | ARA::kARAEditorViewRole;
                    const auto plugInExtensionInstance = documentController->bindDocumentToPluginInstance (*instance.inner,
                                                                                                           allRoles,
                                                                                                           allRoles);
                    playbackRenderer = plugInExtensionInstance.getPlaybackRendererInterface();
                    editorRenderer   = plugInExtensionInstance.getEditorRendererInterface();
                    synchronizeStateWithDocumentController();
                }
                else
                    jassertfalse;
            }
            else
                jassertfalse;
        }

        void getStateInformation (juce::MemoryBlock& b)
        {
            std::lock_guard<std::mutex> configurationLock (instance.innerMutex);

            if (context != nullptr)
                context->getStateInformation (b);
        }

        void setStateInformation (const void* d, int s)
        {
            {
                std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };
                contextUpdateSource = ContextUpdateSource { d, s };
            }

            synchronise();
        }

        ~ARATestHost() override { instance.inner->releaseResources(); }

        void afterProcessBlock (int numSamples)
        {
            const auto isPlayingNow = isPlaying.load();
            playHead.isPlaying.store (isPlayingNow);

            if (isPlayingNow)
            {
                const auto currentAudioSourceLength = audioSourceLength.load();
                const auto currentPlayHeadPosition = playHead.timeInSamples.load();

                // Rudimentary attempt to not seek beyond our sample data, assuming a fairly stable numSamples
                // value. We should gain control over calling the AudioProcessorGraph's processBlock() calls so
                // that we can do sample precise looping.
                if (currentAudioSourceLength - currentPlayHeadPosition < numSamples)
                    playHead.timeInSamples.store (0);
                else
                    playHead.timeInSamples.fetch_add (numSamples);
            }

            if (goToStartSignal.exchange (false))
                playHead.timeInSamples.store (0);
        }

        File getAudioSource() const override
        {
            std::lock_guard<std::mutex> lock { instance.innerMutex };

            if (context != nullptr)
                return context->audioFile;

            return {};
        }

        void setAudioSource (File audioSourceFile) override
        {
            if (audioSourceFile.existsAsFile())
            {
                {
                    std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };
                    contextUpdateSource = ContextUpdateSource (std::move (audioSourceFile));
                }

                synchronise();
            }
        }

        void clearAudioSource() override
        {
            {
                std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };
                contextUpdateSource = ContextUpdateSource (ContextUpdateSource::Type::reset);
            }

            synchronise();
        }

        void setPlaying (bool isPlayingIn) override { isPlaying.store (isPlayingIn); }

        void goToStart() override { goToStartSignal.store (true); }

        Editor* createEditor() { return new Editor (*this); }

        AudioPluginInstance& getAudioPluginInstance() { return instance; }

    private:
        /**  Use this to put the plugin in an unprepared state for the duration of adding and removing PlaybackRegions
             to and from Renderers.
        */
        class ScopedPluginDeactivator
        {
        public:
            explicit ScopedPluginDeactivator (ARAPluginInstanceWrapper& inst) : instance (inst)
            {
                if (instance.prepareToPlayParams.isValid)
                    instance.inner->releaseResources();
            }

            ~ScopedPluginDeactivator()
            {
                if (instance.prepareToPlayParams.isValid)
                    instance.inner->prepareToPlay (instance.prepareToPlayParams.sampleRate,
                                                   instance.prepareToPlayParams.samplesPerBlock);
            }

        private:
            ARAPluginInstanceWrapper& instance;

            JUCE_DECLARE_NON_COPYABLE (ScopedPluginDeactivator)
        };

        class ContextUpdateSource
        {
        public:
            enum class Type
            {
                empty,
                audioSourceFile,
                stateInformation,
                reset
            };

            ContextUpdateSource() = default;

            explicit ContextUpdateSource (const File& file)
                : type (Type::audioSourceFile),
                  audioSourceFile (file)
            {
            }

            ContextUpdateSource (const void* d, int s)
                : type (Type::stateInformation),
                  stateInformation (d, (size_t) s)
            {
            }

            ContextUpdateSource (Type t) : type (t)
            {
                jassert (t == Type::reset);
            }

            Type getType() const { return type; }

            const File& getAudioSourceFile() const
            {
                jassert (type == Type::audioSourceFile);

                return audioSourceFile;
            }

            const MemoryBlock& getStateInformation() const
            {
                jassert (type == Type::stateInformation);

                return stateInformation;
            }

        private:
            Type type = Type::empty;

            File audioSourceFile;
            MemoryBlock stateInformation;
        };

        void synchronise()
        {
            const SpinLock::ScopedLockType scope (instance.innerProcessBlockFlag);
            std::lock_guard<std::mutex> configurationLock (instance.innerMutex);
            synchronizeStateWithDocumentController();
        }

        void synchronizeStateWithDocumentController()
        {
            bool resetContext = false;

            auto newContext = [&]() -> std::unique_ptr<Context>
            {
                std::lock_guard<std::mutex> lock { contextUpdateSourceMutex };

                switch (contextUpdateSource.getType())
                {
                    case ContextUpdateSource::Type::empty:
                        return {};

                    case ContextUpdateSource::Type::audioSourceFile:
                        if (! (contextUpdateSource.getAudioSourceFile().existsAsFile()))
                            return {};

                        {
                            const ARAEditGuard editGuard (documentController->getDocumentController());
                            return std::make_unique<Context> (documentController->getDocumentController(),
                                                              contextUpdateSource.getAudioSourceFile());
                        }

                    case ContextUpdateSource::Type::stateInformation:
                        jassert (contextUpdateSource.getStateInformation().getSize() <= std::numeric_limits<int>::max());

                        return Context::createFromStateInformation (documentController->getDocumentController(),
                                                                    contextUpdateSource.getStateInformation().getData(),
                                                                    (int) contextUpdateSource.getStateInformation().getSize());

                    case ContextUpdateSource::Type::reset:
                        resetContext = true;
                        return {};
                }

                jassertfalse;
                return {};
            }();

            if (newContext != nullptr)
            {
                {
                    ScopedPluginDeactivator deactivator (instance);

                    context = std::move (newContext);
                    audioSourceLength.store (context->fileAudioSource.getFormatReader().lengthInSamples);

                    auto& region = context->playbackRegion.getPlaybackRegion();
                    playbackRenderer.add (region);
                    editorRenderer.add (region);
                }

                sendChangeMessage();
            }

            if (resetContext)
            {
                {
                    ScopedPluginDeactivator deactivator (instance);

                    context.reset();
                    audioSourceLength.store (0);
                }

                sendChangeMessage();
            }
        }

        struct Context
        {
            Context (ARA::Host::DocumentController& dc, const File& audioFileIn)
                : audioFile (audioFileIn),
                  musicalContext    (dc),
                  regionSequence    (dc, musicalContext, "track 1"),
                  fileAudioSource   (dc, audioFile),
                  audioModification (dc, fileAudioSource),
                  playbackRegion    (dc, regionSequence, audioModification, fileAudioSource)
            {
            }

            static std::unique_ptr<Context> createFromStateInformation (ARA::Host::DocumentController& dc, const void* d, int s)
            {
                if (auto xml = getXmlFromBinary (d, s))
                {
                    if (xml->hasTagName (xmlRootTag))
                    {
                        File file { xml->getStringAttribute (xmlAudioFileAttrib) };

                        if (file.existsAsFile())
                            return std::make_unique<Context> (dc, std::move (file));
                    }
                }

                return {};
            }

            void getStateInformation (juce::MemoryBlock& b)
            {
                XmlElement root { xmlRootTag };
                root.setAttribute (xmlAudioFileAttrib, audioFile.getFullPathName());
                copyXmlToBinary (root, b);
            }

            const static Identifier xmlRootTag;
            const static Identifier xmlAudioFileAttrib;

            File audioFile;

            MusicalContext musicalContext;
            RegionSequence regionSequence;
            FileAudioSource fileAudioSource;
            AudioModification audioModification;
            PlaybackRegion playbackRegion;
        };

        SimplePlayHead playHead;
        ARAPluginInstanceWrapper& instance;

        std::unique_ptr<ARAHostDocumentController> documentController;
        ARAHostModel::PlaybackRendererInterface playbackRenderer;
        ARAHostModel::EditorRendererInterface editorRenderer;

        std::unique_ptr<Context> context;

        mutable std::mutex contextUpdateSourceMutex;
        ContextUpdateSource contextUpdateSource;

        std::atomic<bool> isPlaying { false };
        std::atomic<bool> goToStartSignal { false };
        std::atomic<int64> audioSourceLength { 0 };
    };

    explicit ARAPluginInstanceWrapper (std::unique_ptr<AudioPluginInstance> innerIn)
        : inner (std::move (innerIn)), araHost (*this)
    {
        jassert (inner != nullptr);

        for (auto isInput : { true, false })
            matchBuses (isInput);

        setBusesLayout (inner->getBusesLayout());
    }

    //==============================================================================
    AudioProcessorEditor* createARAHostEditor() { return araHost.createEditor(); }

    //==============================================================================
    const String getName() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getName();
    }

    StringArray getAlternateDisplayNames() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getAlternateDisplayNames();
    }

    double getTailLengthSeconds() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getTailLengthSeconds();
    }

    bool acceptsMidi() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->acceptsMidi();
    }

    bool producesMidi() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->producesMidi();
    }

    AudioProcessorEditor* createEditor() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->createEditorIfNeeded();
    }

    bool hasEditor() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->hasEditor();
    }

    int getNumPrograms() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getNumPrograms();
    }

    int getCurrentProgram() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getCurrentProgram();
    }

    void setCurrentProgram (int i) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setCurrentProgram (i);
    }

    const String getProgramName (int i) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->getProgramName (i);
    }

    void changeProgramName (int i, const String& n) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->changeProgramName (i, n);
    }

    void getStateInformation (juce::MemoryBlock& b) override
    {
        XmlElement state ("ARAPluginInstanceWrapperState");

        {
            MemoryBlock m;
            araHost.getStateInformation (m);
            state.createNewChildElement ("host")->addTextElement (m.toBase64Encoding());
        }

        {
            std::lock_guard<std::mutex> lock (innerMutex);

            MemoryBlock m;
            inner->getStateInformation (m);
            state.createNewChildElement ("plugin")->addTextElement (m.toBase64Encoding());
        }

        copyXmlToBinary (state, b);
    }

    void setStateInformation (const void* d, int s) override
    {
        if (auto xml = getXmlFromBinary (d, s))
        {
            if (xml->hasTagName ("ARAPluginInstanceWrapperState"))
            {
                if (auto* hostState = xml->getChildByName ("host"))
                {
                    MemoryBlock m;
                    m.fromBase64Encoding (hostState->getAllSubText());
                    jassert (m.getSize() <= std::numeric_limits<int>::max());
                    araHost.setStateInformation (m.getData(), (int) m.getSize());
                }

                if (auto* pluginState = xml->getChildByName ("plugin"))
                {
                    std::lock_guard<std::mutex> lock (innerMutex);

                    MemoryBlock m;
                    m.fromBase64Encoding (pluginState->getAllSubText());
                    jassert (m.getSize() <= std::numeric_limits<int>::max());
                    inner->setStateInformation (m.getData(), (int) m.getSize());
                }
            }
        }
    }

    void getCurrentProgramStateInformation (juce::MemoryBlock& b) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->getCurrentProgramStateInformation (b);
    }

    void setCurrentProgramStateInformation (const void* d, int s) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setCurrentProgramStateInformation (d, s);
    }

    void prepareToPlay (double sr, int bs) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setRateAndBufferSizeDetails (sr, bs);
        inner->prepareToPlay (sr, bs);
        prepareToPlayParams = { sr, bs };
    }

    void releaseResources() override { inner->releaseResources(); }

    void memoryWarningReceived() override { inner->memoryWarningReceived(); }

    void processBlock (AudioBuffer<float>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlock (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    void processBlock (AudioBuffer<double>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlock (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    void processBlockBypassed (AudioBuffer<float>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlockBypassed (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    void processBlockBypassed (AudioBuffer<double>& a, MidiBuffer& m) override
    {
        const SpinLock::ScopedTryLockType scope (innerProcessBlockFlag);

        if (! scope.isLocked())
            return;

        inner->processBlockBypassed (a, m);
        araHost.afterProcessBlock (a.getNumSamples());
    }

    bool supportsDoublePrecisionProcessing() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->supportsDoublePrecisionProcessing();
    }

    bool supportsMPE() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->supportsMPE();
    }

    bool isMidiEffect() const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->isMidiEffect();
    }

    void reset() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->reset();
    }

    void setNonRealtime (bool b) noexcept override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->setNonRealtime (b);
    }

    void refreshParameterList() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->refreshParameterList();
    }

    void numChannelsChanged() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->numChannelsChanged();
    }

    void numBusesChanged() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->numBusesChanged();
    }

    void processorLayoutsChanged() override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->processorLayoutsChanged();
    }

    void setPlayHead (AudioPlayHead* p) override { ignoreUnused (p); }

    void updateTrackProperties (const TrackProperties& p) override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        inner->updateTrackProperties (p);
    }

    bool isBusesLayoutSupported (const BusesLayout& layout) const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return inner->checkBusesLayoutSupported (layout);
    }

    bool canAddBus (bool) const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return true;
    }
    bool canRemoveBus (bool) const override
    {
        std::lock_guard<std::mutex> lock (innerMutex);
        return true;
    }

    //==============================================================================
    void fillInPluginDescription (PluginDescription& description) const override
    {
        return inner->fillInPluginDescription (description);
    }

private:
    void matchBuses (bool isInput)
    {
        const auto inBuses = inner->getBusCount (isInput);

        while (getBusCount (isInput) < inBuses)
            addBus (isInput);

        while (inBuses < getBusCount (isInput))
            removeBus (isInput);
    }

    // Used for mutual exclusion between the audio and other threads
    SpinLock innerProcessBlockFlag;

    // Used for mutual exclusion on non-audio threads
    mutable std::mutex innerMutex;

    std::unique_ptr<AudioPluginInstance> inner;

    ARATestHost araHost;

    struct PrepareToPlayParams
    {
        PrepareToPlayParams() : isValid (false) {}

        PrepareToPlayParams (double sampleRateIn, int samplesPerBlockIn)
            : isValid (true), sampleRate (sampleRateIn), samplesPerBlock (samplesPerBlockIn)
        {
        }

        bool isValid;
        double sampleRate;
        int samplesPerBlock;
    };

    PrepareToPlayParams prepareToPlayParams;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ARAPluginInstanceWrapper)
};
#endif
